#ifndef MPIH_WIN_HPP
#define MPIH_WIN_HPP

#include "util.hpp"
#include <mpi.h>

class MPI_Win_H {
private:
	MPI_Win m_win;
	bool m_created {false};

public:
	MPI_Win_H() = default;

	/**
	 * @brief Construct a new MPI_Win_H object from any type for which an
	 * overload of MPI_Win_H::create is available.
	 * 
	 * @tparam T 
	 * @param t 
	 * @param comm 
	 * @param info 
	 * 
	 * Currently, an overload for Eigen types is available.
	 */
	template<typename T>
	MPI_Win_H(
		T& t,
		const MPI_Comm& comm,
		const MPI_Info& info = MPI_INFO_NULL
	){
		create(t, comm, info);
	}

private:
	/**
	 * @brief Passes the stored MPI_Win data member to MPI_Win_free.
	 * 
	 * This is called, when a new window is created while an old one is active,
	 * and also during destruction. It is therefore unnecessary to ever call
	 * MPI_Win_free on the managed MPI_Win object.
	 */
	void free(){
		assert(m_created &&
			"MPI_Win_H::free() was called on already freed window!"
			"Do not call MPI_Win_free() on the MPI_Win managed by an MPI_Win_H object!"
		);
		MPI_Win_free(&m_win);
		m_created = false;
	}

public:
	~MPI_Win_H(){
		this->free();
	}

	/**
	 * @brief Performs an MPI fence synchronisation on an MPI_Window by 
	 * passing the stored data member and the assert argument to MPI_Win_fence.
	 * 
	 * @param assert 
	 */
	void fence( int assert = 0 ) const {
		MPI_Win_fence(assert, m_win);
	}

	/**
	 * @brief Creates an MPI Window object using the data pointer and size of
	 * the Eigen object argument.
	 * 
	 * @tparam Derived 
	 * @param m 
	 * @param comm 
	 * @param info 
	 */
	template<typename Derived>
	void create(
		Eigen::DenseBase<Derived>& d,
		const MPI_Comm& comm,
		const MPI_Info& info = MPI_INFO_NULL
	){
		if (m_created){
			this->free();
		}

		constexpr int disp { sizeof(typename Derived::Scalar) };
		MPI_Win_create(
			const_cast<typename Derived::Scalar*>( d.derived().data() ),
			d.derived().size()*disp,disp,
			info,
			comm,
			&m_win
		);

		m_created = true;
	}

	template<typename Derived>
	void get(
		Eigen::DenseBase<Derived>& writeTo, int offset, int count,
		int targetRank, MPI_Aint targetOffset
	) const {
		assert( (writeTo.size() >= offset + count) &&
			"Object to be written to has insufficient size!"
		);
		assert(
			offset >=0 &&
			count >= 0 &&
			targetRank   >= 0 &&
			targetOffset >= 0 &&
			"Offsets, counts and target rank must not be negative!"
		);

		MPI_Get(
			writeTo.derived().data() + offset, count,
			getMPI_Datatype<typename Derived::Scalar>(),
			targetRank, targetOffset, count,
			getMPI_Datatype<typename Derived::Scalar>(),
			m_win
		);
	}

	template<typename Derived>
	void put(
		const Eigen::DenseBase<Derived>& readFrom, int offset, int count,
		int targetRank, MPI_Aint targetOffset
	) const {
		assert( (readFrom.size() >= offset + count) &&
			"Object to be read from has insufficient size!"
		);
		assert(
			offset >=0 &&
			count >= 0 &&
			targetRank   >= 0 &&
			targetOffset >= 0 &&
			"Offsets, counts and target rank must not be negative!"
		);

		MPI_Put(
			readFrom.derived().data() + offset, count,
			getMPI_Datatype<typename Derived::Scalar>(),
			targetRank, targetOffset, count,
			getMPI_Datatype<typename Derived::Scalar>(),
			m_win
		);
	}

	/**
	 * @brief This helper class can be contextually converted to an MPI_Win,
	 * which simply returns the data member.
	 * 
	 * @return const MPI_Win& 
	 */
	operator const MPI_Win&() const { return m_win; }
	/**
	 * @brief Non-const version.
	 * @overload
	 * 
	 * @return MPI_Win& 
	 */
	operator MPI_Win&() { return m_win; }

	/**
	 * @brief This helper class can be contextually converted to bool, which
	 * returns true, if a window was created, and false if not.
	 */
	operator bool() const { return m_created; }

};

#endif